﻿using System;
using System.Collections.Generic;
using System.Text;
using Vortex.Wrapper.Direct3D;
using System.Drawing;
using Vortex.Wrapper;
using Vortex.Debugging;
using System.Runtime.InteropServices;

namespace Vortex.Drawing {

	/// <summary>
	/// Represents texture/backbuffer surface object. Surface pixels can be modified or read
	/// </summary>
	public sealed class Surface {
		private DrawingDevice _device;
		private Direct3DSurface9 _surface;
		private Size _size;
		private PixelFormat _format;
		private int _lockCount;

		internal Surface(DrawingDevice device, Direct3DSurface9 surface) {
			_device = device;
			_surface = surface;

			//read surface description
			SurfaceDesc description = _surface.Description;
			_size = new Size((int)description.Width, (int)description.Height);
			_format = (PixelFormat)description.Format;
		}

		/// <summary>
		/// Frees this surface instance.
		/// </summary>
		internal void Free() {
			_surface.Dispose();
		}

		/// <summary>
		/// Gets the API reference of surface.
		/// </summary>
		/// <value>The API reference of surface.</value>
		internal Direct3DSurface9 ApiRef {
			get { return _surface; }
		}

		/// <summary>
		/// Gets the size of surface.
		/// </summary>
		/// <value>The surface size.</value>
		public Size Size {
			get { return _size; }
		}


		/// <summary>
		/// Gets the region which describes an entire surface.
		/// </summary>
		/// <value>The surface region.</value>
		public Rectangle Region {
			get { return new Rectangle(Point.Empty, _size); }
		}

		/// <summary>
		/// Gets the surface format.
		/// </summary>
		/// <value>The surface format.</value>
		public PixelFormat Format {
			get { return _format; }
		}

		#region Set Data Methods

		/// <summary>
		/// Sets the surface data.
		/// </summary>
		/// <param name="data">The data to set.</param>
		/// <param name="dataFormat">Pixel format which represent user data structure. Size of should match.</param>
		public void SetData<T>(T[] data, PixelFormat dataFormat) where T : struct {
			if (null == data) throw new ArgumentNullException("data");
			SetData(data, 0, data.Length, dataFormat);
		}

		/// <summary>
		/// Sets the surface data.
		/// </summary>
		/// <param name="data">The data to set.</param>
		/// <param name="startIndex">The index of first data element.</param>
		/// <param name="elementCount">The element count.</param>
		/// <param name="dataFormat">Pixel format which represent user data structure. Size of should match.</param>
		public void SetData<T>(T[] data, int startIndex, int elementCount, PixelFormat dataFormat) where T : struct {
			if (null == data) throw new ArgumentNullException("data");
			SetData(Region, data, startIndex, elementCount, dataFormat);
		}

		/// <summary>
		/// Sets the surface data.
		/// </summary>
		/// <param name="targetRegion">The target surface region.</param>
		/// <param name="data">The data to set.</param>
		/// <param name="dataFormat">Pixel format which represent user data structure. Size of should match.</param>
		public void SetData<T>(Rectangle targetRegion, T[] data, PixelFormat dataFormat) where T : struct {
			if (null == data) throw new ArgumentNullException("data");

			SetData(targetRegion, data, 0, data.Length, dataFormat);
		}

		/// <summary>
		/// Sets the surface data.
		/// </summary>
		/// <param name="targetRegion">The target surface region.</param>
		/// <param name="data">The data to set.</param>
		/// <param name="startIndex">The index of first data element.</param>
		/// <param name="elementCount">The element count.</param>
		/// <param name="dataFormat">Pixel format which represent user data structure. Size of should match.</param>
		public void SetData<T>(Rectangle region, T[] data, int startIndex, int elementCount, PixelFormat dataFormat) where T : struct {
			SetGetData(true, region, data, startIndex, elementCount, dataFormat);
		}

		#endregion

		#region Get Data Methods

		/// <summary>
		/// Gets pixel data from entire surface. User data structure should match pixel format size of this surface
		/// </summary>
		/// <param name="dataFormat">Pixel format which represent user data structure. Size of should match.</param>
		/// <returns>Array of user data structures with surface data</returns>
		public T[] GetData<T>(PixelFormat dataFormat) where T : struct {
			Rectangle region = new Rectangle(Point.Empty, Size);
			T[] data = new T[region.Width * region.Height];
			GetData(data, dataFormat);
			return data;
		}

		/// <summary>
		/// Gets pixel data from region of surface. User data structure should match pixel format size of this surface
		/// </summary>
		/// <param name="region">The source region of surface.</param>
		/// <param name="dataFormat">Pixel format which represent user data structure. Size of should match.</param>
		/// <returns>
		/// Array of user data structures with surface data
		/// </returns>
		public T[] GetData<T>(Rectangle sourceRegion, PixelFormat dataFormat) where T : struct {
			if (sourceRegion.Width <= 0 || sourceRegion.Height <= 0) {
				throw new ArgumentException(string.Format("Specified region '{0}' is empty", sourceRegion));
			}
			T[] data = new T[sourceRegion.Width * sourceRegion.Height];
			GetData(sourceRegion, data, dataFormat);
			return data;
		}

		/// <summary>
		/// Gets pixel data from region of surface. User data structure should match pixel format size of this surface
		/// </summary>
		/// <param name="data">The data array of user structures to be filled with pixels data. Array size should be equal to surface width * height</param>
		/// <param name="dataFormat">Pixel format which represent user data structure. Size of should match.</param>		
		public void GetData<T>(T[] data, PixelFormat dataFormat) where T : struct {
			if (null == data) throw new ArgumentNullException("data");
			GetData(data, 0, data.Length, dataFormat);
		}

		/// <summary>
		/// Gets pixel data from region of surface. User data structure should match pixel format size of this surface
		/// </summary>
		/// <param name="data">The data array of user structures to be filled with pixels data.</param>
		/// <param name="startIndex">The start index of array to fill.</param>
		/// <param name="elementCount">The element count to fill. Element count should match surface width * height.</param>
		/// <param name="dataFormat">Pixel format which represent user data structure. Size of should match.</param>		
		public void GetData<T>(T[] data, int startIndex, int elementCount, PixelFormat dataFormat) where T : struct {
			if (null == data) throw new ArgumentNullException("data");
			GetData(Region, data, startIndex, elementCount, dataFormat);
		}

		/// <summary>
		/// Gets pixel data from region of surface. User data structure should match pixel format size of this surface
		/// </summary>
		/// <param name="region">The source region of surface to extract.</param>
		/// <param name="data">The data array of user structures to be filled with pixels data.</param>
		/// <param name="dataFormat">Pixel format which represent user data structure. Size of should match.</param>		
		public void GetData<T>(Rectangle region, T[] data, PixelFormat dataFormat) where T : struct {
			if (null == data) throw new ArgumentNullException("data");
			GetData(region, data, 0, data.Length, dataFormat);
		}

		/// <summary>
		/// Gets pixel data from region of surface. User data structure should match pixel format size of this surface
		/// </summary>
		/// <param name="region">The source region of surface to extract.</param>
		/// <param name="data">The data array of user structures to be filled with pixels data.</param>
		/// <param name="startIndex">The start index of array to fill.</param>
		/// <param name="elementCount">The element count to fill. Element count should match surface width * height.</param>
		/// <param name="dataFormat">Pixel format which represent user data structure. Size of should match.</param>		
		public void GetData<T>(Rectangle region, T[] data, int startIndex, int elementCount, PixelFormat dataFormat) where T : struct {
			if (null == data) throw new ArgumentNullException("data");
			SetGetData(false, region, data, startIndex, elementCount, dataFormat);
		}

		#endregion

		/// <summary>
		/// Gets or sets pixel data from region of surface. User data structure should match pixel format size of this surface
		/// </summary>
		/// <param name="setOperation">if set to <c>true</c> then set operation will be performed.</param>
		/// <param name="region">The source region of surface to extract/set.</param>
		/// <param name="data">The data array of user structures which contains pixels data.</param>
		/// <param name="startIndex">The start index of array to fill/read.</param>
		/// <param name="elementCount">The element count to fill/read. Element count should match surface width * height.</param>
		/// <param name="dataFormat">Pixel format which represent user data structure. Size of should match.</param>
		private void SetGetData<T>(bool setOperation, Rectangle region, T[] data, int startIndex, int elementCount, PixelFormat dataFormat) where T : struct {
			if (data == null) throw new ArgumentException("data");

			if (!Region.Contains(region) || region.Width <= 0 || region.Height <= 0) {
				throw new ArgumentException(string.Format("Specified region {0} for SetData is empty is out of texture bounds {1}", region, Region));
			}

			if (elementCount + startIndex > data.Length) {
				throw new ArgumentException(string.Format("Array index out of range. Length = {0}, but startIndex = {1}, elementCount = {2}", data.Length, startIndex, elementCount));
			}

			if (elementCount != region.Width * region.Height) {
				throw new ArgumentException(string.Format("Invalid element count. Element count specified = {0}, but required count = {1} ({2} x {3})", data.Length, region.Width * region.Height, region.Width, region.Height));
			}

			//prepare some parameters
			Rectangle zeroRegion = new Rectangle(Point.Empty, region.Size);
			int userElementSize = Marshal.SizeOf(typeof(T));
			int pixelSize = ImageHelper.GetPixelSize(dataFormat);

			if (userElementSize != pixelSize) {
				throw new ArgumentException(string.Format("Data structure '{0}' has size '{1}' which is not equal to pixel size'{1}' of format {2}", typeof(T).FullName, userElementSize, pixelSize, dataFormat));
			}

			unsafe {
				var handle = GCHandle.Alloc(data, GCHandleType.Pinned);
				void* pinnedMemory = handle.AddrOfPinnedObject().ToPointer();

				if (setOperation) {
					HResult result = D3DX.LoadSurfaceFromMemory(_surface, region, pinnedMemory, (D3DFormat)dataFormat, (uint)(region.Width * userElementSize), zeroRegion, D3DX.Default, 0);
					Watcher.AssertCritical(result, "LoadSurfaceFromMemory for set T[] data");
				} else {
					//IDEA: 
					//1) create temp surface
					//2) load data onto it from this surface (as region)
					//3) lock bits of temp surface and copy them directly to buffer

					Direct3DSurface9 tempSurface;
					HResult result = _device.ApiRef.CreateOffscreenPlainSurface((uint)region.Width, (uint)region.Height, (D3DFormat)dataFormat, D3DPool.SystemMemory, out tempSurface);
					Watcher.AssertCritical(result, "CreateOffscreenPlainSurface for get get[] data");

					result = D3DX.LoadSurfaceFromSurface(tempSurface, zeroRegion, _surface, region, D3DX.Default, 0);
					Watcher.AssertCritical(result, "LoadSurfaceFromMemory for get T[] data");

					//now temp surface contains pixel data converted to required format
					D3DLockedRect tempLockedRect;
					result = tempSurface.LockRect(zeroRegion, D3DLock.ReadOnly, out tempLockedRect);
					Watcher.AssertCritical(result, "Unable to lock temp surface");

					//copy data to dest array
					byte* pSource = (byte*)tempLockedRect.pBits;
					byte* pDest = (byte*)pinnedMemory;
					int destStride = (userElementSize * zeroRegion.Width);

					for (int n = 0; n < zeroRegion.Height; ++n ) {
						for (int m = 0; m < destStride; ++m) {
							pDest[m] = pSource[m];
						}
						pDest += destStride;
						pSource += tempLockedRect.Pitch;
					}

					tempSurface.UnlockRect();
					tempSurface.Dispose();					
				}
				//unpin data
				handle.Free();
			}
		}

		#region Bitmap Data Manipulations

		/// <summary>
		/// Gets the bitmap data.
		/// </summary>
		/// <param name="targetBitmap">The target bitmap. Size of bitmap should be exact as surface size</param>
		public void GetBitmapData(Bitmap targetBitmap) {
			GetBitmapData(targetBitmap, Region);
		}

		/// <summary>
		/// Gets the bitmap data.
		/// </summary>
		/// <param name="targetBitmap">The target bitmap. Its size should be equal to source region size</param>
		/// <param name="source">The source region on texture. Region should be within surface region.</param>
		public void GetBitmapData(Bitmap targetBitmap, Rectangle source) {
			if (null == targetBitmap) throw new ArgumentNullException("targetBitmap");
			GetBitmapData(new Rectangle(Point.Empty, targetBitmap.Size), targetBitmap, source);
		}

		/// <summary>
		/// Gets the bitmap data.
		/// </summary>
		/// <param name="targetRegion">The target region of bitmap to be filled.</param>
		/// <param name="targetBitmap">The target bitmap.</param>
		/// <param name="sourceRegion">The source region of surface to copy.</param>
		public void GetBitmapData(Rectangle targetRegion, Bitmap targetBitmap, Rectangle sourceRegion) {
			if (null == targetBitmap) throw new ArgumentNullException("targetBitmap");

			Rectangle zeroRegion = new Rectangle(Point.Empty, targetBitmap.Size);

			if (!Region.Contains(sourceRegion)) {
				throw new ArgumentException(string.Format("Source region '{0}' is out of range surface region '{1}'", sourceRegion, Region));
			}

			if (!zeroRegion.Contains(targetRegion)) {
				throw new ArgumentException(string.Format("Target region '{0}' is out of range taret bitmap region '{1}'", targetRegion, zeroRegion));
			}

			//step N1, create offscreen plain surface in memory of bitmap compatible format
			PixelFormat compFormat = ImageHelper.FindMatchingFormat(targetBitmap.PixelFormat);
			if (compFormat == PixelFormat.Unknown) {
				throw new ArgumentException(string.Format("Bitmap format '{0}' can't be reslved from surface format '{1}'", targetBitmap.PixelFormat, Format));
			}

			Direct3DSurface9 tempSurface;
			HResult result = _device.ApiRef.CreateOffscreenPlainSurface((uint)targetRegion.Width, (uint)targetRegion.Height, (D3DFormat)compFormat, D3DPool.SystemMemory, out tempSurface);
			Watcher.AssertCritical(result, "CreateOffscreenPlainSurface for get GetBitmapData data");

			result = D3DX.LoadSurfaceFromSurface(tempSurface, zeroRegion, _surface, sourceRegion, D3DX.Default, 0);
			Watcher.AssertCritical(result, "LoadSurfaceFromMemory for get GetBitmapData data");

			//now temp surface contains pixel data converted to required format
			D3DLockedRect tempLockedRect;
			result = tempSurface.LockRect(zeroRegion, D3DLock.ReadOnly, out tempLockedRect);
			Watcher.AssertCritical(result, "Unable to lock temp surface");
			{
				System.Drawing.Imaging.BitmapData bmpData = targetBitmap.LockBits(targetRegion, System.Drawing.Imaging.ImageLockMode.WriteOnly, targetBitmap.PixelFormat);
				unsafe {
					//copy data to dest array
					byte* pSource = (byte*)tempLockedRect.pBits;
					byte* pDest = (byte*)bmpData.Scan0.ToPointer();
					int destStride = (ImageHelper.GetPixelSize(compFormat) * zeroRegion.Width);

					for (int n = 0; n < zeroRegion.Height; ++n) {
						for (int m = 0; m < destStride; ++m) {
							pDest[m] = pSource[m];
						}
						pDest += destStride;
						pSource += tempLockedRect.Pitch;
					}
				}
				targetBitmap.UnlockBits(bmpData);
			}

			tempSurface.UnlockRect();
			tempSurface.Dispose();
		}

		/// <summary>
		/// Converts surface data into the bitmap.
		/// </summary>
		/// <returns><see cref="System.Drawing.Bitmap"/> with surface data</returns>
		public Bitmap ToBitmap() {
			return ToBitmap(Region);
		}

		/// <summary>
		/// Converts some region of surface data into the bitmap.
		/// </summary>
		/// <param name="source">The source region on surface.</param>
		/// <returns><see cref="System.Drawing.Bitmap"/> with part of surface data</returns>
		public Bitmap ToBitmap(Rectangle source) {
			return ToBitmap(source.Size, source);
		}

		/// <summary>
		/// Converts some region of surface data into the bitmap.
		/// </summary>
		/// <param name="bitmapSize">Size of the result bitmap.</param>
		/// <param name="source">The source region on surface.</param>
		/// <returns>
		/// 	<see cref="System.Drawing.Bitmap"/> with part of surface data
		/// </returns>
		public Bitmap ToBitmap(Size bitmapSize, Rectangle source) {
			if (bitmapSize.Width < 1 || bitmapSize.Height < 1) {
				throw new ArgumentException("Bitmap size components (width, height) should be greater than 1");
			}

			if (Region.Contains(source)) {
				System.Drawing.Imaging.PixelFormat pixelFormat = ImageHelper.FindMatchingFormat(Format);
				if (pixelFormat != System.Drawing.Imaging.PixelFormat.Undefined) {
					Bitmap bitmap = new Bitmap(bitmapSize.Width, bitmapSize.Height, pixelFormat);
					GetBitmapData(new Rectangle(Point.Empty, bitmapSize), bitmap, source);
					return bitmap;
				} else {
					throw new Exception(string.Format("Surface with format {0} can't be blitted to bitmap", Format));
				}
			} else {
				throw new ArgumentException(string.Format("Region {0} is out of surface region {1}", source, Region));
			}
		}


		/// <summary>
		/// Fill the this surface with the bitmap data.
		/// </summary>
		/// <param name="sourceBitmap">The source bitmap.</param>
		public void SetBitmapData(Bitmap sourceBitmap) {
			SetBitmapData(Region, sourceBitmap);
		}

		/// <summary>
		/// Fill the target surface region with the bitmap data.
		/// </summary>
		/// <param name="targetRegion">The target surface region.</param>
		/// <param name="sourceBitmap">The source bitmap.</param>
		public void SetBitmapData(Rectangle targetRegion, Bitmap sourceBitmap) {
			if (null == sourceBitmap) throw new ArgumentNullException("bitmap");
			SetBitmapData(targetRegion, sourceBitmap, new Rectangle(Point.Empty, sourceBitmap.Size));
		}

		/// <summary>
		/// Sets the bitmap data.
		/// </summary>
		/// <param name="targetRegion">The target surface region.</param>
		/// <param name="sourceBitmap">The source bitmap.</param>
		/// <param name="sourceRegion">The source bitmap region.</param>
		public void SetBitmapData(Rectangle targetRegion, Bitmap sourceBitmap, Rectangle sourceRegion) {
			Rectangle bitmapRect = new Rectangle(Point.Empty, sourceBitmap.Size);

			if (!bitmapRect.Contains(sourceRegion)) throw new ArgumentException("Source " + sourceRegion + " is out of bitmap extent " + bitmapRect);
			if (!Region.Contains(targetRegion)) throw new ArgumentException("Target " + targetRegion + " is out of texture extent " + targetRegion);

			Bitmap tempBitmap = null;
			PixelFormat srcFormat = ImageHelper.FindMatchingFormat(sourceBitmap.PixelFormat);

			//if source format is not 1:1 than we need to proxy image onto known format
			if (srcFormat == PixelFormat.Unknown) {
				tempBitmap = new Bitmap(sourceBitmap.Width, sourceBitmap.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
				using (Graphics tempGraphics = Graphics.FromImage(tempBitmap)) {
					tempGraphics.DrawImageUnscaled(sourceBitmap, 0, 0);
				}
				sourceBitmap = tempBitmap;
				srcFormat = ImageHelper.FindMatchingFormat(tempBitmap.PixelFormat);
			}

			//locking source bitmap
			System.Drawing.Imaging.BitmapData bitmapData = sourceBitmap.LockBits(
				sourceRegion,
				System.Drawing.Imaging.ImageLockMode.ReadOnly,
				sourceBitmap.PixelFormat
			);

			unsafe {
				HResult result = D3DX.LoadSurfaceFromMemory(_surface, targetRegion, (void*)bitmapData.Scan0, (D3DFormat)srcFormat, (uint)bitmapData.Stride, sourceRegion, D3DX.Default, 0);
				Watcher.AssertCritical(result, "LoadSurfaceFromMemory for setting surface data from bitmap");
			}

			sourceBitmap.UnlockBits(bitmapData);

			//free temp bitmap if format conversion was performed
			if (null != tempBitmap) {
				tempBitmap.Dispose();
			}
		}

		#endregion

		#region Graphics Management

		/// <summary>
		/// Locks the surface for obtaining GDI+ graphics object targeted on this surface. After painting is complete surface should be unlocked by ReleaseGraphics, otherwise failure is expected.
		/// </summary>
		/// <returns><see cref="System.Drawing.Graphics"/> instance if surface was locked successfully; otherwise <c>null</c></returns>
		public Graphics LockGraphics() {
			IntPtr hDC;
			if (_surface.GetDC(out hDC).IsSuccess) {
				_lockCount++;
				return Graphics.FromHdc(hDC);
			} else {
				return null;
			}
		}

		/// <summary>
		/// Unlocks surface locked previously the graphics.
		/// </summary>
		/// <param name="graphics">The graphics returned by LockGraphics method.</param>
		public void UnlockGraphics(Graphics graphics) {
			if (null == graphics) throw new ArgumentNullException("graphics");

			if (_surface.ReleaseDC(graphics.GetHdc()).IsSuccess) {
				_lockCount--;
			} else {
				Log.Warning("Unsuccessful unlocking of surface");
			}
		}

		#endregion
	}

}
